/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.function.Function;

public class threeDRaytrace {
    
    public static double[] createVector(double x, double y, double z) {
        return new double[] { x, y, z };
    }

    public static double lengthVector(double[] self) {
        return Math.sqrt(self[0] * self[0] + self[1] * self[1] + self[2] * self[2]);
    }

    public static double[] addVector(double[] self, double[] v) {
        self[0] += v[0];
        self[1] += v[1];
        self[2] += v[2];
        return self;
    }

    public static double[] subVector(double[] self, double[] v) {
        self[0] -= v[0];
        self[1] -= v[1];
        self[2] -= v[2];
        return self;
    }

    public static double[] scaleVector(double[] self, double scale) {
        self[0] *= scale;
        self[1] *= scale;
        self[2] *= scale;
        return self;
    }

    public static double[] normaliseVector(double[] self) {
        double len = Math.sqrt(self[0] * self[0] + self[1] * self[1] + self[2] * self[2]);
        self[0] /= len;
        self[1] /= len;
        self[2] /= len;
        return self;
    }

    public static double[] add(double[] v1, double[] v2) {
        return new double[] { v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2] };
    }

    public static double[] sub(double[] v1, double[] v2) {
        return new double[] { v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2] };
    }

    public static double[] scalev(double[] v1, double[] v2) {
        return new double[] { v1[0] * v2[0], v1[1] * v2[1], v1[2] * v2[2] };
    }

    public static double dot(double[] v1, double[] v2) {
        return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
    }

    public static double[] scale(double[] v, double scale) {
        return new double[] { v[0] * scale, v[1] * scale, v[2] * scale };
    }

    public static double[] cross(double[] v1, double[] v2) {
        return new double[] { v1[1] * v2[2] - v1[2] * v2[1],
                v1[2] * v2[0] - v1[0] * v2[2],
                v1[0] * v2[1] - v1[1] * v2[0] };
    }

    public static double[] normalise(double[] v) {
        double len = lengthVector(v);
        return new double[] { v[0] / len, v[1] / len, v[2] / len };
    }

    public static double[] transformMatrix(double[] self, double[] v) {
        double[] vals = self;
        double x = vals[0] * v[0] + vals[1] * v[1] + vals[2] * v[2] + vals[3];
        double y = vals[4] * v[0] + vals[5] * v[1] + vals[6] * v[2] + vals[7];
        double z = vals[8] * v[0] + vals[9] * v[1] + vals[10] * v[2] + vals[11];
        return new double[] { x, y, z };
    }

    public static double[] invertMatrix(double[] self) {
        double[] temp = new double[16];
        double tx = -self[3];
        double ty = -self[7];
        double tz = -self[11];
        for (int h = 0; h < 3; h++) {
            for (int v = 0; v < 3; v++) {
                temp[h + v * 4] = self[v + h * 4];
            }
        }
        for (int i = 0; i < 11; i++) {
            self[i] = temp[i];
        }
        self[3] = tx * self[0] + ty * self[1] + tz * self[2];
        self[7] = tx * self[4] + ty * self[5] + tz * self[6];
        self[11] = tx * self[8] + ty * self[9] + tz * self[10];
        return self;
    }
}

class TriangleClass {
    public int axis = 0;
    public double[] normal = new double[0];
    public double nu = 0.0;
    public double nv = 0.0;
    public double nd = 0.0;
    public double eu = 0.0;
    public double ev = 0.0;
    public double nu1 = 0.0;
    public double nv1 = 0.0;
    public double nu2 = 0.0;
    public double nv2 = 0.0;
    public double[] material = new double[0];

    public Function<double[], double[]> shader = (pos) -> {
        double x = ((pos[0] / 32) % 2 + 2) % 2;
        double z = ((pos[2] / 32 + 0.3) % 2 + 2) % 2;
        if ((x < 1) != (z < 1)) {
            return threeDRaytrace.createVector(0.4, 0.4, 0.4);
        } else {
            return threeDRaytrace.createVector(0.0, 0.4, 0.0);
        }
    };

    public boolean isEmpty = false;
    public boolean hasShader = false;
    TriangleClass(double[] p1, double[] p2, double[] p3) {
       new TriangleClass(p1, p2, p3, false);
    }
    TriangleClass(double[] p1, double[] p2, double[] p3, boolean isEmpty) {
        this.isEmpty = isEmpty;
        double[] edge1 = threeDRaytrace.sub(p3, p1);
        double[] edge2 = threeDRaytrace.sub(p2, p1);
        double[] normal = threeDRaytrace.cross(edge1, edge2);
        if (Math.abs(normal[0]) > Math.abs(normal[1])) {
            if (Math.abs(normal[0]) > Math.abs(normal[2])) {
                this.axis = 0;
            } else {
                this.axis = 2;
            }
        } else {
            if (Math.abs(normal[1]) > Math.abs(normal[2])) {
                this.axis = 1;
            } else {
                this.axis = 2;
            }
        }
        int u = (this.axis + 1) % 3;
        int v = (this.axis + 2) % 3;
        double u1 = edge1[u];
        double v1 = edge1[v];

        double u2 = edge2[u];
        double v2 = edge2[v];
        this.normal = threeDRaytrace.normalise(normal);
        this.nu = normal[u] / normal[this.axis];
        this.nv = normal[v] / normal[this.axis];
        this.nd = threeDRaytrace.dot(normal, p1) / normal[this.axis];
        double det = u1 * v2 - v1 * u2;
        this.eu = p1[u];
        this.ev = p1[v];
        this.nu1 = u1 / det;
        this.nv1 = -v1 / det;
        this.nu2 = v2 / det;
        this.nv2 = -u2 / det;
        this.material = new double[] { 0.7, 0.7, 0.7 };
    }

    public Object intersect(double[] orig, double[] dir, double near, double far) {
        int u = (this.axis + 1) % 3;
        int v = (this.axis + 2) % 3;
        double d = dir[this.axis] + this.nu * dir[u] + this.nv * dir[v];
        double t = (this.nd - orig[this.axis] - this.nu * orig[u] - this.nv * orig[v]) / d;
        if (t < near || t > far) {
            return null;
        }
        double Pu = orig[u] + t * dir[u] - this.eu;
        double Pv = orig[v] + t * dir[v] - this.ev;
        double a2 = Pv * this.nu1 + Pu * this.nv1;
        if (a2 < 0) {
            return null;
        }
        double a3 = Pu * this.nu2 + Pv * this.nv2;
        if (a3 < 0) {
            return null;
        }
        if ((a2 + a3) > 1) {
            return null;
        }
        return t;
    }
}

class LightsClass {
    double[] arr = new double[0];
    double[] colour = new double[0];
    LightsClass() {}
}

class SceneClass {
    public TriangleClass closest = new TriangleClass(new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 },
            new double[] { 0, 0, 0 }, true);
    public TriangleClass[] triangles;
    public LightsClass[] lights = new LightsClass[3];
    public double[] ambient = new double[] { 0, 0, 0 };
    public double[] background = new double[] { 0.8, 0.8, 1 };

    SceneClass(TriangleClass[] a_triangles) {
        this.triangles = a_triangles;
    }

    public double[] intersect(double[] origin, double[] dir, double near, double far0) {
        double far = far0;
        for (int i = 0; i < triangles.length; i++) {
            TriangleClass triangle = triangles[i];
            Object d = triangle.intersect(origin, dir, near, far);
            if (d == null || (double) d > far || (double) d < near)
                continue;                                               
            far = (double) d;
            closest = triangle;
        }

        if (closest.isEmpty) {
            return new double[] { background[0], background[1], background[2] };
        }

        double[] normal = closest.normal;
        double[] hit = threeDRaytrace.add(origin, threeDRaytrace.scale(dir, far));
        if (threeDRaytrace.dot(dir, normal) > 0) {
            normal = new double[] { -normal[0], -normal[1], -normal[2] };
        }

        double[] colour = new double[0];
        if (closest.hasShader) {
            colour = closest.shader.apply(hit);
        } else {
            colour = closest.material;
        }

        double[] l = new double[] { this.ambient[0], this.ambient[1], this.ambient[2] };
        for (int i = 0; i < this.lights.length; i++) {
            LightsClass light = this.lights[i];
            double[] toLight = threeDRaytrace.sub(light.arr, hit);
            double distance = threeDRaytrace.lengthVector(toLight);
            threeDRaytrace.scaleVector(toLight, 1.0 / distance);
            distance -= 0.0001;
            if (this.blocked(hit, toLight, distance)) {
                continue;
            }
            double nl = threeDRaytrace.dot(normal, toLight);
            if (nl > 0) {
                threeDRaytrace.addVector(l, threeDRaytrace.scale(light.colour, nl));
            }
        }
        l = threeDRaytrace.scalev(l, colour);

        return l;
    }

    public boolean blocked(double[] O, double[] D, double far) {
        double near = 0.0001;
        for (int i = 0; i < this.triangles.length; i++) {
            TriangleClass triangle = this.triangles[i];
            Object d = triangle.intersect(O, D, near, far);
            if (d == null || (double) d > far || (double) d < near) {
                continue;
            }
            return true;
        }
        return false;
    }
}

class RayClass {
    public double[] origin = new double[0];
    public double[] dir = new double[0];
    RayClass() {}
}

class CameraClass {
    public double[] origin = new double[0];
    public double[][] directions = new double[4][];

    CameraClass(double[] origin, double[] lookat, double[] up) {
        double[] zaxis = threeDRaytrace.normaliseVector(threeDRaytrace.subVector(lookat, origin));
        double[] xaxis = threeDRaytrace.normaliseVector(threeDRaytrace.cross(up, zaxis));
        double[] yaxis = threeDRaytrace.normaliseVector(
                threeDRaytrace.cross(xaxis, threeDRaytrace.subVector(new double[] { 0, 0, 0 }, zaxis)));
        double[] m = new double[16];
        m[0] = xaxis[0];
        m[1] = xaxis[1];
        m[2] = xaxis[2];
        m[4] = yaxis[0];
        m[5] = yaxis[1];
        m[6] = yaxis[2];
        m[8] = zaxis[0];
        m[9] = zaxis[1];
        m[10] = zaxis[2];
        threeDRaytrace.invertMatrix(m);
        m[3] = 0;
        m[7] = 0;
        m[11] = 0;
        this.origin = origin;
        this.directions[0] = threeDRaytrace.normalise(new double[] { -0.7, 0.7, 1 });
        this.directions[1] = threeDRaytrace.normalise(new double[] { 0.7, 0.7, 1 });
        this.directions[2] = threeDRaytrace.normalise(new double[] { 0.7, -0.7, 1 });
        this.directions[3] = threeDRaytrace.normalise(new double[] { -0.7, -0.7, 1 });
        this.directions[0] = threeDRaytrace.transformMatrix(m, this.directions[0]);
        this.directions[1] = threeDRaytrace.transformMatrix(m, this.directions[1]);
        this.directions[2] = threeDRaytrace.transformMatrix(m, this.directions[2]);
        this.directions[3] = threeDRaytrace.transformMatrix(m, this.directions[3]);
    }

    public RayClass[] generateRayPair(double y) {
        RayClass[] rays = new RayClass[2];
        rays[0] = new RayClass();
        rays[1] = new RayClass();
        rays[0].origin = this.origin;
        rays[1].origin = this.origin;
        rays[0].dir = threeDRaytrace.addVector(threeDRaytrace.scale(this.directions[0], y),
                threeDRaytrace.scale(this.directions[3], 1 - y));
        rays[1].dir = threeDRaytrace.addVector(threeDRaytrace.scale(this.directions[1], y),
                threeDRaytrace.scale(this.directions[2], 1 - y));
        return rays;
    }

    public void render(SceneClass scene, double[][][] pixels, int width, int height) {
        CameraClass cam = this;
        renderRows(cam, scene, pixels, width, height, 0, height);
    }

    public void renderRows(CameraClass camera, SceneClass scene, double[][][] pixels, int width, int height, int starty,
            int stopy) {
        // int pointNum = 1;
        for (int y = starty; y < stopy; y++) {
            RayClass[] rays = camera.generateRayPair(y / height);
            for (int x = 0; x < width; x++) {
                double xp = (double) x / (double) width;
                double[] origin = threeDRaytrace.addVector(threeDRaytrace.scale(rays[0].origin, xp),
                        threeDRaytrace.scale(rays[1].origin, 1 - xp));
                double[] dir = threeDRaytrace.normaliseVector(threeDRaytrace.addVector(threeDRaytrace.scale(rays[0].dir, xp), threeDRaytrace.scale(rays[1].dir, 1 - xp)));
                double[] l = scene.intersect(origin, dir, 0, 0);
                pixels[y][x] = l;
                // if (pointNum % 5== 0) {
                //     System.out.println("pointNum = " + pointNum + " point l :" + l[0] + ", " + l[1] + ", " + l[2]);
                // }
                // pointNum += 1;
            }
        }
    }

}
/*
 * @State
 * 
 * @Tags Jetstream2
 */
class Benchmark {

    public static void main(String[] args) throws Exception {
        new Benchmark().runThreeDRaytrace();
    }

    public double[][][] raytraceScene() {
        TriangleClass[] triangles = new TriangleClass[14];
        double[] tfl = threeDRaytrace.createVector(-10, 10, -10);
        double[] tfr = threeDRaytrace.createVector(10, 10, -10);
        double[] tbl = threeDRaytrace.createVector(-10, 10, 10);
        double[] tbr = threeDRaytrace.createVector(10, 10, 10);
        double[] bfl = threeDRaytrace.createVector(-10, -10, -10);
        double[] bfr = threeDRaytrace.createVector(10, -10, -10);
        double[] bbl = threeDRaytrace.createVector(-10, -10, 10);
        double[] bbr = threeDRaytrace.createVector(10, -10, 10);
        
        // cube!
        // front
        int i = 0;

        triangles[i++] = new TriangleClass(tfl, tfr, bfr);
        triangles[i++] = new TriangleClass(tfl, bfr, bfl);
        // back
        triangles[i++] = new TriangleClass(tbl, tbr, bbr);
        triangles[i++] = new TriangleClass(tbl, bbr, bbl);
        // triangles[i-1].material = [0.7,0.2,0.2];
        // triangles[i-1].material.reflection = 0.8;
        // left
        triangles[i++] = new TriangleClass(tbl, tfl, bbl);
        // triangles[i-1].reflection = 0.6;
        triangles[i++] = new TriangleClass(tfl, bfl, bbl);
        // triangles[i-1].reflection = 0.6;
        // right
        triangles[i++] = new TriangleClass(tbr, tfr, bbr);
        triangles[i++] = new TriangleClass(tfr, bfr, bbr);
        // top
        triangles[i++] = new TriangleClass(tbl, tbr, tfr);
        triangles[i++] = new TriangleClass(tbl, tfr, tfl);
        // bottom
        triangles[i++] = new TriangleClass(bbl, bbr, bfr);
        triangles[i++] = new TriangleClass(bbl, bfr, bfl);

        double[] ffl = threeDRaytrace.createVector(-1000, -30, -1000);
        double[] ffr = threeDRaytrace.createVector(1000, -30, -1000);
        double[] fbl = threeDRaytrace.createVector(-1000, -30, 1000);
        double[] fbr = threeDRaytrace.createVector(1000, -30, 1000);
        triangles[i++] = new TriangleClass(fbl, fbr, ffr);
        triangles[i - 1].hasShader = true;
        triangles[i++] = new TriangleClass(fbl, ffr, ffl);
        triangles[i - 1].hasShader = true;
        SceneClass scene = new SceneClass(triangles);
        scene.lights[0] = new LightsClass();
        scene.lights[0].arr = threeDRaytrace.createVector(20, 38, -22);
        scene.lights[0].colour = threeDRaytrace.createVector(0.7, 0.3, 0.3);
        scene.lights[1] = new LightsClass();
        scene.lights[1].arr = threeDRaytrace.createVector(-23, 40, 17);
        scene.lights[1].colour = threeDRaytrace.createVector(0.7, 0.3, 0.3);
        scene.lights[2] = new LightsClass();
        scene.lights[2].arr = threeDRaytrace.createVector(23, 20, 17);
        scene.lights[2].colour = threeDRaytrace.createVector(0.7, 0.7, 0.7);
        scene.ambient = threeDRaytrace.createVector(0.1, 0.1, 0.1);
        // _scene.background = createVector(0.7, 0.7, 1.0);

        int size = 30;
        double[][][] pixels = new double[30][][];
        for (int y = 0; y < size; y++) {
            pixels[y] = new double[30][];
            for (int x = 0; x < size; x++) {
                pixels[y][x] = threeDRaytrace.createVector(0, 0, 0);
            }
        }
        CameraClass camera = new CameraClass(threeDRaytrace.createVector(-40, 40, 40),
                threeDRaytrace.createVector(0, 0, 0), threeDRaytrace.createVector(0, 1, 0));
        camera.render(scene, pixels, size, size);
        return pixels;
    }

    public void runThreeDRaytrace() {
        double start = Timer.now();
        raytraceScene();
        double end = Timer.now();
        double time = (double) (end - start) / 1000000;
        System.out.println("Array Access - RunThreeDRaytrace:\t" + time+"\tms");
    }
}

class Timer {
    public static double now() {
        return System.nanoTime();
    }
}